# ============================================================================
# 文件路径: src/packages.nim
# 文件用途: 包数据模型和搜索逻辑
#
# 功能说明:
# 1. 定义包数据结构和搜索结果类型
# 2. 实现多种搜索算法 (简单搜索、评分搜索、标签搜索)
# 3. 提供包数据的 CRUD 操作 (从 Redis 读取)
# 4. 实现智能搜索评分系统，提高搜索结果相关性
# 5. 支持 JSON 序列化，便于 API 响应
# ============================================================================

# 导入标准库
import std/[
  algorithm,    # 排序算法
  options,      # 可选类型处理
  json,         # JSON 序列化
  strutils,     # 字符串处理工具
  times         # 时间处理
]

# 导入项目依赖
import ready      # Redis 客户端
import ./cache    # Redis 连接池

# ============================================================================
# 数据类型定义
# ============================================================================

type
  Package* = object
    ## Nim 包的完整信息结构
    ## 包含基本信息和仓库元数据
    name*: string              # 包名 (唯一标识符)
    url*: string               # 仓库 URL (Git/Mercurial 等)
    mmethod*: string           # 获取方法 (git, hg 等)
    description*: string       # 包描述
    license*: string           # 开源许可证
    web*: string              # 官方网站链接
    tags*: seq[string]        # 标签列表 (用于分类)
    
    # 仓库元数据 (通过 API 获取)
    latestVersion*: string     # 最新版本号
    latestUpdateDate*: string  # 最后更新日期
    repoLastChecked*: string   # 最后检查时间
    repoPlatform*: string      # 托管平台 (GitHub/GitLab/Codeberg)

type
  SearchResult* = object
    ## 搜索结果结构，包含包信息和相关性评分
    package*: Package          # 包的完整信息
    score*: float             # 相关性评分 (0-100)
    matchType*: string        # 匹配类型 (exact_name, name_contains 等)

# ============================================================================
# 包数据访问函数
# ============================================================================

proc getPackage*(name: string): Package =
  ## 根据包名从 Redis 获取完整的包信息
  ## 
  ## 参数:
  ##   name: 包名
  ## 返回:
  ##   Package 对象，如果包不存在则返回空对象
  ##
  ## Redis 数据结构:
  ## - "package:包名" -> 哈希表存储基本信息
  ## - "package:包名:tags" -> 集合存储标签列表
  try:
    cachePool.withConnection conn:
      # 获取包的基本信息 (存储为 Redis 哈希)
      let hash = conn.command("HGETALL", "package:" & name)
      let hashArray = hash.to(seq[Option[string]])
      
      if hashArray.len > 0:
        # Redis HGETALL 返回 [key1, value1, key2, value2, ...] 格式
        # 需要成对处理键值对
        for i in countup(0, hashArray.len - 2, 2):
          let key = hashArray[i].get("")
          let value = hashArray[i + 1]
          if value.isSome:
            # 根据键名设置对应的包字段
            case key
            of "name": result.name = value.get
            of "url": result.url = value.get
            of "method": result.mmethod = value.get
            of "description": result.description = value.get
            of "license": result.license = value.get
            of "web": result.web = value.get
            of "latest_version": result.latestVersion = value.get
            of "latest_update_date": result.latestUpdateDate = value.get
            of "repo_last_checked": result.repoLastChecked = value.get
            of "repo_platform": result.repoPlatform = value.get

        # 获取包的标签列表 (存储为 Redis 集合)
        let tags = conn.command("SMEMBERS", "package:" & name & ":tags")
        result.tags = tags.to(seq[string])
  except CatchableError:
    # Redis 连接失败时返回空对象
    result = Package()

proc getAllPackages*(): seq[Package] =
  ## 获取所有包
  try:
    cachePool.withConnection conn:
      let packageNames = conn.command("SMEMBERS", "package_names")
      let names = packageNames.to(seq[string])
      result = @[]
      for name in names:
        result.add(getPackage(name))
  except CatchableError:
    # Redis 连接失败时返回空列表
    result = @[]

proc searchPackages*(query: string): seq[Package] =
  ## 使用高质量匹配搜索包 (评分 40+)
  try:
    cachePool.withConnection conn:
      let packageNames = conn.command("SMEMBERS", "package_names")
      let names = packageNames.to(seq[string])
      result = @[]
      let queryLower = query.toLowerAscii()

      for name in names:
        let package = getPackage(name)
        let nameLower = package.name.toLowerAscii()
        let descLower = package.description.toLowerAscii()

        # 高质量匹配 (评分 40+)
        # 精确名称匹配 (100)
        if queryLower == nameLower:
          result.add(package)
          continue
        # 名称以查询开头 (90)
        elif nameLower.startsWith(queryLower):
          result.add(package)
          continue
        # 查询是名称的子字符串 (80)
        elif queryLower in nameLower:
          result.add(package)
          continue
        # 描述包含查询 (50)
        elif queryLower in descLower:
          result.add(package)
          continue
        # 精确标签匹配 (45)
        else:
          for tag in package.tags:
            let tagLower = tag.toLowerAscii()
            if queryLower == tagLower:
              result.add(package)
              break
  except CatchableError:
    # Redis 连接失败时返回空列表
    result = @[]

proc searchPackagesByTag*(tag: string): seq[Package] =
  ## 根据标签搜索包
  try:
    cachePool.withConnection conn:
      let packageNames = conn.command("SMEMBERS", "tag:" & tag)
      let names = packageNames.to(seq[string])
      result = @[]

      for name in names:
        result.add(getPackage(name))
  except CatchableError:
    # Redis 连接失败时返回空列表
    result = @[]

proc searchPackagesWithScore*(query: string): seq[SearchResult] =
  ## 智能搜索算法 - 使用多维度评分系统提供高质量搜索结果
  ## 
  ## 参数:
  ##   query: 搜索查询字符串
  ## 返回:
  ##   按相关性评分排序的搜索结果列表 (只包含评分 40+ 的高质量匹配)
  ##
  ## 评分规则:
  ## - 精确名称匹配: 100 分 (最高优先级)
  ## - 名称以查询开头: 90 分
  ## - 名称包含查询: 80 分  
  ## - 描述单词匹配: 40 分
  ## - 精确标签匹配: 45 分
  ## - 部分匹配: 10-40 分
  ##
  ## 质量过滤: 只返回评分 >= 40 的结果，确保搜索结果的相关性
  try:
    cachePool.withConnection conn:
      # 获取所有包名列表
      let packageNames = conn.command("SMEMBERS", "package_names")
      let names = packageNames.to(seq[string])
      result = @[]
      let queryLower = query.toLowerAscii()  # 转换为小写进行不区分大小写匹配

      # 遍历每个包进行评分
      for name in names:
        let package = getPackage(name)
        let nameLower = package.name.toLowerAscii()
        let descLower = package.description.toLowerAscii()
        var score = 0.0
        var matchType = ""

        # ================================================================
        # 名称匹配评分 (最重要的匹配维度)
        # ================================================================
        if queryLower == nameLower:
          # 精确名称匹配 - 最高优先级
          score = 100.0
          matchType = "exact_name"
        elif nameLower.startsWith(queryLower):
          # 名称以查询开头 - 高优先级
          score = 90.0
          matchType = "name_starts_with"
        elif queryLower in nameLower:
          # 查询是名称的子字符串 - 中等优先级
          score = 80.0
          matchType = "name_contains"
        elif nameLower in queryLower:
          # 名称是查询的子字符串 - 较低优先级
          score = 40.0
          matchType = "name_partial"

        # ================================================================
        # 描述匹配评分 (基于单词分割的智能匹配)
        # ================================================================
        let descWords = descLower.split({' ', '-', '_', '.', ',', ';', ':', '!'})
        for word in descWords:
          if queryLower in word:
            score += 40.0
            matchType = "description_word_contains"
            break
          elif word in queryLower:
            score += 15.0
            matchType = "description_word_partial"
            break

        # ================================================================
        # 标签匹配评分 (支持分类搜索)
        # ================================================================
        for tag in package.tags:
          let tagLower = tag.toLowerAscii()
          if queryLower == tagLower:
            score += 45.0
            matchType = "exact_tag"
            break
          elif queryLower in tagLower:
            score += 10.0
            matchType = "tag_contains"

        # ================================================================
        # 质量过滤：只包含高质量匹配 (评分 40+)
        # ================================================================
        if score >= 40.0:
          result.add(SearchResult(package: package, score: score, matchType: matchType))

      # 按评分降序排序 (最相关的结果在前)
      result.sort(proc(a, b: SearchResult): int =
        if a.score > b.score: -1 elif a.score < b.score: 1 else: 0
      )
  except CatchableError:
    # Redis 连接失败时返回空结果
    result = @[]

proc getPackageCount*(): int =
  ## 获取包的总数
  try:
    cachePool.withConnection conn:
      let count = conn.command("GET", "packages_count")
      let countOpt = count.to(Option[int])
      if countOpt.isSome:
        result = countOpt.get
      else:
        result = 0
  except CatchableError:
    # Redis 连接失败时返回 0
    result = 0

proc getLastUpdated*(): int =
  ## 获取最后更新的时间戳
  try:
    cachePool.withConnection conn:
      let timestamp = conn.command("GET", "last_updated")
      let timestampOpt = timestamp.to(Option[int])
      if timestampOpt.isSome:
        result = timestampOpt.get
      else:
        # 没有数据时返回当前时间
        result = getTime().toUnix()
  except CatchableError:
    # Redis 连接失败时返回当前时间
    result = getTime().toUnix()



proc toJson*(package: Package): JsonNode =
  ## 将包转换为 JSON
  result = %*{
    "name": package.name,
    "url": package.url,
    "method": package.mmethod,
    "description": package.description,
    "license": package.license,
    "web": package.web,
    "tags": package.tags,
    "latest_version": package.latestVersion,
    "latest_update_date": package.latestUpdateDate,
    "repo_last_checked": package.repoLastChecked,
    "repo_platform": package.repoPlatform
  }

proc toJson*(data: SearchResult): JsonNode =
  ## 将搜索结果转换为 JSON
  result = %*{
    "package": toJson(data.package),
    "score": data.score,
    "matchType": data.matchType
  }
